import { isNil, uniqueId } from 'lodash-es';

class EventBusListener<T = unknown> {
  static create<T = unknown>(
    sn: Partial<{
      id: string;
      exec: (event: CustomEvent<T>) => void;
      options: Partial<{
        once: boolean;
      }>;
    }>
  ) {
    const {
      id = uniqueId('event-bus-listener-'),
      exec = () => {},
      options = {
        once: false,
      },
    } = sn;
    return new EventBusListener(id, exec, options);
  }

  constructor(
    public readonly id: string,
    public readonly exec: (event: CustomEvent<T>) => void,
    public readonly options: Partial<{
      once: boolean;
    }>
  ) {}
}

class EventBus {
  static instances = new Map<string, EventBus>([]);

  private listeners = new Map<string, Array<EventBusListener<any>>>([]);

  constructor(public readonly name: string) {}

  on<T = unknown>(type: string, listener: (event: CustomEvent<T>) => void) {
    const handlers = this.listeners.get(type) || [];
    handlers.push(
      EventBusListener.create<T>({
        exec: listener,
      })
    );
    this.listeners.set(type, handlers);
  }

  once<T = unknown>(type: string, listener: (event: CustomEvent<T>) => void) {
    const handlers = this.listeners.get(type) || [];
    handlers.push(
      EventBusListener.create<T>({
        exec: listener,
        options: {
          once: true,
        },
      })
    );
    this.listeners.set(type, handlers);
  }

  off<T = unknown>(type: string, listener: (event: CustomEvent<T>) => void) {
    const handlers = this.listeners.get(type) || [];
    const found = handlers.find((i) => i.exec === listener);
    if (!found) {
      return;
    }
    this.listeners.set(
      type,
      handlers.filter((j) => j.id !== found.id)
    );
  }

  emit<T = unknown>(type: string, detail?: T) {
    const items = this.listeners.get(type) || [];
    for (const i of items) {
      i.exec(new CustomEvent(type, { detail }));
    }
  }
}

function useEventBus(name: string = 'unis') {
  const instance = EventBus.instances.get(name);
  if (!isNil(instance)) {
    return instance;
  }

  const bus = new EventBus(name);
  EventBus.instances.set(name, bus);
  return bus;
}

export { useEventBus };
